package org.ix.dynamicapis.plugins;

import org.aopalliance.aop.Advice;
import org.ix.dynamicapis.utils.JsonUtils;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class PluginsFactory implements ApplicationContextAware {

    private final Map<String, PluginConfig> configs = new HashMap<>();

    private final Map<String, Advice> adviceCache = new HashMap<>();

    ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    public Collection<PluginConfig> flushConfigs() throws IOException {
        String configPath = "/Users/xi/Projects/gitee.com/dynamic-apis/plugins/plugins.json";
        String configJson = new String(Files.readAllBytes(Paths.get(configPath)), StandardCharsets.UTF_8);
        List<PluginConfig> pluginConfigs = JsonUtils.parseList(configJson, PluginConfig.class);
        if (pluginConfigs != null) {
            for (PluginConfig pluginConfig : pluginConfigs) {
                if (!configs.containsKey(pluginConfig.getId())) {
                    configs.put(pluginConfig.getId(), pluginConfig);
                }
            }
        }
        return configs.values();
    }

    public Map<String, PluginConfig> getConfigs() {
        return configs;
    }

    public void enable(String pluginId) {
        if (!configs.containsKey(pluginId)) {
            throw new RuntimeException("插件不存在");
        }
        PluginConfig config = configs.get(pluginId);
        for (String name : applicationContext.getBeanDefinitionNames()) {
            Object bean = applicationContext.getBean(name);
            if (bean == this || !(bean instanceof Advised) || findAdvice((Advised) bean, config.getClassName()) != null) continue;

            try {
                ((Advised) bean).addAdvice(buildAdvice(config));
            } catch (Exception e) {
                throw new RuntimeException(e.getMessage());
            }
        }
    }

    public void disable(String pluginId) {
        if (!configs.containsKey(pluginId)) {
            throw new RuntimeException("插件不存在");
        }
        PluginConfig config = configs.get(pluginId);
        config.setActive(false);
        for (String name : applicationContext.getBeanDefinitionNames()) {
            Object bean = applicationContext.getBean(name);
            if (bean instanceof Advised) {
                Advice advice = findAdvice((Advised) bean, config.getClassName());
                if (advice != null) {
                    ((Advised) bean).removeAdvice(advice);
                }
            }
        }
    }

    private Advice findAdvice(Advised advised, String className) {
        for (Advisor advisor : advised.getAdvisors()) {
            if (advisor.getAdvice().getClass().getName().equals(className)) {
                return advisor.getAdvice();
            }
        }
        return null;
    }

    private Advice buildAdvice(PluginConfig config) throws Exception {
        if (adviceCache.containsKey(config.getClassName())) {
            return adviceCache.get(config.getClassName());
        }
        URL targetUrl = new URL(config.getJarRemoteUrl());
        URLClassLoader loader = (URLClassLoader) getClass().getClassLoader();
        boolean isLoader = false;
        for (URL url : loader.getURLs()) {
            if (url.equals(targetUrl)) {
                isLoader = true;
                break;
            }
        }

        if (!isLoader) {
            Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", new Class[]{URL.class});
            addURL.setAccessible(true);
            addURL.invoke(loader, targetUrl);
        }
        Class<?> adviceClass = loader.loadClass(config.getClassName());
        Advice advice = (Advice) adviceClass.newInstance();
        adviceCache.put(config.getClassName(), advice);
        return advice;
    }
}
